/**
 * Copyright(c) 2014 Xunton Solutions
 *
 * History:
 *   15-9-24 上午9:53 Created by dwzhang
 */
package com.jade.bss.weixin.api.component;

import java.io.StringReader;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Named;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;

import com.jade.bss.weixin.WeixinApi;
import com.jade.bss.weixin.WeixinApp;
import com.jade.bss.weixin.WeixinException;
import com.jade.bss.weixin.api.component.event.ComponentEvent;
import com.jade.bss.weixin.api.component.event.UnauthorizedEvent;
import com.jade.bss.weixin.api.component.event.VerifyTicketEvent;
import com.jade.bss.weixin.api.component.protocol.*;
import com.jade.bss.weixin.api.protocol.json.Response;
import com.jade.framework.base.ErrorConstants;
import com.jade.framework.base.util.HttpUtils;
import com.jade.framework.cache.CacheService;
import com.jade.framework.cache.CacheUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * 代公众号调用接口．公众号在登录授权给第三方平台后，许多公众号业务的实现，需要依靠调用接口来实现。本接口介绍了
 * 公众号第三方平台开发者在授权流程中需要用到的API，以及获得公众号的授权后，如何代替公众号调用接口。
 *
 * @author <a href="mailto:dwzhang@xunton.com">dwzhang</a> 
 * @version 1.0 15-9-24 上午9:53
 */
@Named("bss_weixinComponentApi")
public class ComponentApi
        extends WeixinApi
{
    private static final String URL_COMPONENT = "https://api.weixin.qq.com/cgi-bin/component/";
    private static final String URL_ACCESS_TOKEN = URL_COMPONENT + "api_component_token";
    private static final String URL_PRE_AUTH_CODE = URL_COMPONENT + "api_create_preauthcode";
    private static final String URL_QUERY_AUTH = URL_COMPONENT + "api_query_auth";
    private static final String URL_REFRESH_TOKEN = URL_COMPONENT + "api_authorizer_token";
    private static final String URL_GET_AUTHINFO = URL_COMPONENT + "api_get_authorizer_info";
    private static final String URL_GET_OPTION = URL_COMPONENT + "api_get_authorizer_option";
    private static final String URL_SET_OPTION = URL_COMPONENT + "api_set_authorizer_option";
    private static String PREFIX_CACHE_COMPONENT = "bss.weixin.component";
    private CacheService cacheService;
    private ComponentListener listener;
    private Log log = LogFactory.getLog(ComponentApi.class);

    @Inject
    public void setCacheService(@Named ("system_cacheService") CacheService cacheService)
    {
        this.cacheService = cacheService;
    }

    /**
     * 获取第三方平台的Access Token，并把它保存在Cache中。
     *
     * @param appCode 第三方平台代码。
     * @return 第三方平台的Access Token
     * @throws WeixinException 处理异常
     */
    public String getAccessToken(String appCode)
            throws WeixinException
    {
        return getAccessToken(getApp(appCode));
    }

    /**
     * 获取第三方平台的Access Token，并把它保存在Cache中。
     *
     * @param app 第三方平台app信息。
     * @return 第三方平台的Access Token
     * @throws WeixinException 处理异常
     */
    public String getAccessToken(WeixinApp app)
            throws WeixinException
    {
        //check if in cache
        String key = CacheUtils.joinKey(PREFIX_CACHE_COMPONENT, "token", app.getAppId());
        String token = cacheService.get(key);
        if (StringUtils.isNotBlank(token)) {
            return token;
        }
        String ticket = getVerifyTicket(app);
        if (StringUtils.isBlank(ticket)) {
            throw new WeixinException("err.bss.weixin.api.component.invalid_ticket", "no ticket");
        }
        //invoke weixin interface
        Map<String, String> params = new HashMap<String, String>();
        params.put("component_appid", app.getAppId());
        params.put("component_appsecret", app.getAppSecret());
        params.put("component_verify_ticket", ticket);

        AccessTokenResponse response = submitRequest(URL_PRE_AUTH_CODE, params, AccessTokenResponse.class);
        cacheService.set(key, response.getAccessToken(), response.getExpiresIn());
        return response.getAccessToken();
    }

    /**
     * 获取预授权码。
     *
     * @param appCode 第三方应用代码
     * @param accessToken 访问令牌
     * @return 预授权码
     * @throws WeixinException 处理异常
     */
    public String getPreAuthCode(String appCode, String accessToken)
            throws WeixinException
    {
        return getPreAuthCode(getApp(appCode), accessToken);
    }

    /**
     * 获取预授权码。
     *
     * @param app 第三方平台应用
     * @param accessToken 访问令牌
     * @return 预授权码
     * @throws WeixinException 处理异常
     */
    public String getPreAuthCode(WeixinApp app, String accessToken)
            throws WeixinException
    {
        //invoke weixin interface
        Map<String, String> params = new HashMap<String, String>();
        params.put("component_appid", app.getAppId());
        PreAuthCodeResponse response = submitRequest(URL_PRE_AUTH_CODE + "?component_access_token=" + accessToken,
                params, PreAuthCodeResponse.class);
        return response.getPreAuthCode();
    }

    /**
     * 使用授权码换取公众号的授权信息。该API用于使用授权码换取授权公众号的授权信息，并换取authorizer_access_token和
     * authorizer_refresh_token。 授权码的获取，需要在用户在第三方平台授权页中完成授权流程后，在回调URI中通过URL参数
     * 提供给第三方平台方。
     *
     * @param appCode 第三方应用代码
     * @param accessToken 访问Token
     * @param authCode 验证码
     * @return 授权信息
     * @throws WeixinException 调用异常
     */
    public AuthorizationInfo queryAuth(String appCode, String accessToken, String authCode)
            throws WeixinException
    {
        return queryAuth(getApp(appCode), accessToken, authCode);
    }

    /**
     * 使用授权码换取公众号的授权信息。该API用于使用授权码换取授权公众号的授权信息，并换取authorizer_access_token和
     * authorizer_refresh_token。 授权码的获取，需要在用户在第三方平台授权页中完成授权流程后，在回调URI中通过URL参数
     * 提供给第三方平台方。
     *
     * @param app 第三方平台应用
     * @param accessToken 访问Token
     * @param authCode 验证码
     * @return 授权信息
     * @throws WeixinException 调用异常
     */
    public AuthorizationInfo queryAuth(WeixinApp app, String accessToken, String authCode)
            throws WeixinException
    {
        Map<String, String> params = new HashMap<String, String>();
        params.put("component_appid", app.getAppId());
        params.put("authorization_code", authCode);
        QueryAuthResponse response = submitRequest(URL_QUERY_AUTH + "?component_access_token=" + accessToken,
                params, QueryAuthResponse.class);
        return response.getInfo();
    }

    /**
     * 获取授权方的账户信息. 该API用于获取授权方的公众号基本信息，包括头像、昵称、帐号类型、认证类型、微信号、原始ID
     * 和二维码图片URL。
     *
     * @param appCode 第三方应用代码
     * @param accessToken 访问Token
     * @param authorizerAppId 授权方应用
     * @return 授权信息
     * @throws WeixinException  处理异常
     */
    public AuthorizerInfo getAuthorizerInfo(String appCode, String accessToken, String authorizerAppId)
            throws WeixinException
    {
        return getAuthorizerInfo(getApp(appCode), accessToken, authorizerAppId);
    }

    /**
     * 获取授权方的账户信息. 该API用于获取授权方的公众号基本信息，包括头像、昵称、帐号类型、认证类型、微信号、原始ID
     * 和二维码图片URL。
     *
     * @param app 第三方平台应用
     * @param accessToken 访问Token
     * @param authorizerAppId 授权方应用
     * @return 授权信息
     * @throws WeixinException  处理异常
     */
    public AuthorizerInfo getAuthorizerInfo(WeixinApp app, String accessToken, String authorizerAppId)
            throws WeixinException
    {
        //invoke weixin interface
        Map<String, String> params = new HashMap<String, String>();
        params.put("component_appid", app.getAppId());
        params.put("authorizer_appid", authorizerAppId);
        AuthorizerInfoResponse response = submitRequest(URL_GET_AUTHINFO + "?component_access_token=" + accessToken,
                params, AuthorizerInfoResponse.class);
        return response.getAuthorizerInfo();
    }

    /**
     *  获取（刷新）授权公众号的令牌. 该API用于在授权方令牌（authorizer_access_token）失效时，可用刷新令牌
     * （authorizer_refresh_token）获取新的令牌。
     *
     * @param appCode 第三方应用代码
     * @param accessToken 访问Token
     * @param authorizerAppId 授权方appid
     * @param refreshToken 授权方的刷新令牌
     * @return 刷新令牌
     * @throws WeixinException 处理异常
     */
    public String refreshAuthToken(String appCode, String accessToken, String authorizerAppId, String refreshToken)
            throws WeixinException
    {
        return refreshAuthToken(getApp(appCode), accessToken, authorizerAppId, refreshToken);
    }

    /**
     *  获取（刷新）授权公众号的令牌. 该API用于在授权方令牌（authorizer_access_token）失效时，可用刷新令牌
     * （authorizer_refresh_token）获取新的令牌。
     *
     * @param app 第三方平台应用
     * @param accessToken 访问Token
     * @param authorizerAppId 授权方appid
     * @param refreshToken 授权方的刷新令牌
     * @return 刷新令牌
     * @throws WeixinException 处理异常
     */
    public String refreshAuthToken(WeixinApp app, String accessToken, String authorizerAppId, String refreshToken)
            throws WeixinException
    {
        Map<String, String> params = new HashMap<String, String>();
        params.put("component_appid", app.getAppId());
        params.put("authorizer_appid", authorizerAppId);
        params.put("authorizer_refresh_token", refreshToken);
        RefreshTokenResponse response = submitRequest(URL_REFRESH_TOKEN + "?component_access_token=" + accessToken,
                params, RefreshTokenResponse.class);
        return response.getAuthorizerAccessToken();
    }

    /**
     * 获取授权方的选项设置信息．该API用于获取授权方的公众号的选项设置信息，如：地理位置上报，语音识别开关，
     * 多客服开关。注意，获取各项选项设置信息，需要有授权方的授权，详见权限集说明。
     *
     * @param appCode 第三方应用代码
     * @param accessToken 访问Token
     * @param authorizerAppId 授权方appid
     * @param optionName 选项名称
     * @return 选项值
     * @throws WeixinException 处理异常
     */
    public String getOption(String appCode, String accessToken, String authorizerAppId, String optionName)
            throws WeixinException
    {
        return getOption(getApp(appCode), accessToken, authorizerAppId, optionName);
    }

    /**
     * 获取授权方的选项设置信息．该API用于获取授权方的公众号的选项设置信息，如：地理位置上报，语音识别开关，
     * 多客服开关。注意，获取各项选项设置信息，需要有授权方的授权，详见权限集说明。
     *
     * @param app 第三方平台应用
     * @param accessToken 访问Token
     * @param authorizerAppId 授权方appid
     * @param optionName 选项名称
     * @return 选项值
     * @throws WeixinException 处理异常
     */
    public String getOption(WeixinApp app, String accessToken, String authorizerAppId, String optionName)
            throws WeixinException
    {
        Map<String, String> params = new HashMap<String, String>();
        params.put("component_appid", app.getAppId());
        params.put("authorizer_appid", authorizerAppId);
        params.put("option_name", optionName);
        GetOptionResponse response = submitRequest(URL_GET_OPTION + "?component_access_token=" + accessToken,
                params, GetOptionResponse.class);
        return response.getOptionValue();
    }

    /**
     *  设置授权方的选项信息。该API用于设置授权方的公众号的选项信息，如：地理位置上报，语音识别开关，多客服开关。
     *  注意，设置各项选项设置信息，需要有授权方的授权，详见权限集说明。
     *
     * @param appCode 第三方应用代码
     * @param accessToken 访问Token
     * @param authorizerAppId 授权方appid
     * @param optionName 选项名称
     * @param optionValue 选项值
     * @throws WeixinException 处理异常
     */
    public void setOption(String appCode, String accessToken, String authorizerAppId, String optionName,
                          String optionValue)
            throws WeixinException
    {
        setOption(getApp(appCode), accessToken, authorizerAppId, optionName, optionValue);
    }

    /**
     *  设置授权方的选项信息。该API用于设置授权方的公众号的选项信息，如：地理位置上报，语音识别开关，多客服开关。
     *  注意，设置各项选项设置信息，需要有授权方的授权，详见权限集说明。
     *
     * @param app 第三方平台应用
     * @param accessToken 访问Token
     * @param authorizerAppId 授权方appid
     * @param optionName 选项名称
     * @param optionValue 选项值
     * @throws WeixinException 处理异常
     */
    public void setOption(WeixinApp app, String accessToken, String authorizerAppId, String optionName,
                          String optionValue)
            throws WeixinException
    {
        //invoke weixin interface
        Map<String, String> params = new HashMap<String, String>();
        params.put("component_appid", app.getAppId());
        params.put("authorizer_appid", authorizerAppId);
        params.put("option_name", optionName);
        params.put("option_value", optionValue);
        submitRequest(URL_SET_OPTION + "?component_access_token=" + accessToken, params, Response.class);
    }

    /**
     * 设置Component Ticket.
     *
     * @param appCode 第三方应用代码
     * @param ticket component ticket
     */
    public void setVerifyTicket(String appCode, String ticket)
    {
        setVerifyTicket(getApp(appCode), ticket);
    }

    /**
     * 设置Component Ticket.
     *
     * @param app 第三方应用
     * @param ticket component ticket
     */
    public void setVerifyTicket(WeixinApp app, String ticket)
    {
        String key = CacheUtils.joinKey(PREFIX_CACHE_COMPONENT, "ticket", app.getAppId());
        cacheService.set(key, ticket, 1200);
    }

    /**
     * 获取component ticket.
     *
     * @param appCode 第三方应用代码
     * @return component ticket
     */
    public String getVerifyTicket(String appCode)
    {
        return getVerifyTicket(getApp(appCode));
    }

    /**
     * 获取component ticket.
     *
     * @param app 第三方应用
     * @return component ticket
     */
    public String getVerifyTicket(WeixinApp app)
    {
        String key = CacheUtils.joinKey(PREFIX_CACHE_COMPONENT, "ticket", app.getAppId());
        return cacheService.get(key);
    }

    protected <T extends Response> T submitRequest(String url, Object object, Class<T> clazz)
            throws WeixinException
    {
        try {
            T response = HttpUtils.postJsonHttp(URL_ACCESS_TOKEN, object, clazz);
            if (!response.isOK()) {
                throw handleErrorResposne(response);
            }
            else {
                return response;
            }
        }
        catch (Exception e) {
            throw handleException(e);
        }
    }

    protected WeixinException handleErrorResposne(Response response)
            throws WeixinException
    {
        return new WeixinException("err.bss.weixin.api.component.server",
                response.getErrorCode() + ":" + response.getErrorMsg());
    }

    protected WeixinException handleException(Exception e)
            throws WeixinException
    {
        if (e instanceof WeixinException) {
            return (WeixinException) e;
        }
        else {
            return new WeixinException(ErrorConstants.ERR_NETWORK, e.getMessage(), e);
        }
    }

    /**
     * 处理Component相关的事件，包括取消授权、Ticket通知等。
     *
     * @param event component事件
     * @throws WeixinException 处理错误
     */
    public void handleEvent(WeixinApp app, ComponentEvent event)
            throws WeixinException
    {
        if (event instanceof VerifyTicketEvent) {
            log.info(
                    "Set component ticket to " + ((VerifyTicketEvent) event).getTicket() + " for app " + event.getAppId());

            if (app == null) {
                log.warn("Can't find configuration for component app " + event.getAppId());
            }
            else {
                setVerifyTicket(app, ((VerifyTicketEvent) event).getTicket());
            }
        }
        else if (event instanceof UnauthorizedEvent) {
            log.info(
                    "Unauthorize the app " + ((UnauthorizedEvent) event).getAuthorizerAppid() + " for component " + event.getAppId());
            if (listener != null) {
                listener.onEvent((UnauthorizedEvent) event);
            }
        }
    }

    /**
     * 根据XML内容解析事件对象。
     *
     * @param xml 事件xml内容。
     * @return 事件对象
     * @throws WeixinException 解析异常
     */
    public <E extends ComponentEvent> E parse(String xml)
            throws WeixinException
    {
        ComponentEvent event = null;
        if (StringUtils.indexOfIgnoreCase(xml, "component_verify_ticket") > 0) {
            event = parse(xml, VerifyTicketEvent.class);

        }
        else if (StringUtils.indexOfIgnoreCase(xml, "unauthorized") > 0) {
            event = parse(xml, UnauthorizedEvent.class);
        }
        else {
            log.warn("Invalid event:" + xml);
        }
        return (E) event;
    }

    protected <E extends ComponentEvent> E parse(String xmlStr, Class classz)
            throws WeixinException
    {
        try {
            JAXBContext context = JAXBContext.newInstance(classz);
            Unmarshaller unmarshaller = context.createUnmarshaller();
            StringReader reader = new StringReader(xmlStr);
            return (E) unmarshaller.unmarshal(reader);
        }
        catch (Exception e) {
            throw new WeixinException("err.bss.weixin.api.component.parse_xml", e.getMessage());
        }
    }
}
